package com.ruoyi.framework.web.service;

import java.io.Serializable;
import java.util.Date;
import java.util.HashMap;
import java.util.Map;
import java.util.concurrent.TimeUnit;
import java.util.function.Supplier;

import cn.hutool.core.util.ObjectUtil;
import cn.hutool.json.JSONUtil;
import com.fasterxml.jackson.annotation.JsonFormat;
import com.ruoyi.common.utils.StringUtils;
import com.ruoyi.common.utils.ip.IpUtils;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import org.apache.poi.ss.formula.functions.T;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.security.core.Authentication;
import org.springframework.stereotype.Component;
import com.ruoyi.common.constant.CacheConstants;
import com.ruoyi.common.constant.Constants;
import com.ruoyi.common.core.domain.entity.SysUser;
import com.ruoyi.common.core.redis.RedisCache;
import com.ruoyi.common.exception.user.UserPasswordNotMatchException;
import com.ruoyi.common.exception.user.UserPasswordRetryLimitExceedException;
import com.ruoyi.common.utils.MessageUtils;
import com.ruoyi.common.utils.SecurityUtils;
import com.ruoyi.framework.manager.AsyncManager;
import com.ruoyi.framework.manager.factory.AsyncFactory;
import com.ruoyi.framework.security.context.AuthenticationContextHolder;

import javax.validation.constraints.NotNull;

/**
 * 登录密码方法
 * 
 * @author ruoyi
 */
@Component("sysPasswordService")
public class SysPasswordService
{
    @Autowired
    private RedisCache redisCache;

    @Value(value = "${user.password.maxRetryCount}")
    private int maxRetryCount;

    @Value(value = "${user.password.lockTime}")
    private int lockTime;

    private final String DEFAULT_LOCK_TYPE = "login";

    private Map<TimeUnit, String> timeStringMap = new HashMap<>();
    {
        timeStringMap.put(TimeUnit.DAYS, "days");
        timeStringMap.put(TimeUnit.SECONDS, "seconds");
        timeStringMap.put(TimeUnit.MINUTES, "minutes");
        timeStringMap.put(TimeUnit.HOURS, "hours");
    }

    /**
     * 登录账户密码错误次数缓存键名 (pwd_err_cnt:Ip地址:用户名)
     *
     * @param username 用户名
     * @return 缓存键key
     */
    protected String getCacheKey(String username, String lockType) {
        String ipAddr = IpUtils.getIpAddr();
        return CacheConstants.PWD_ERR_CNT_KEY + ipAddr + ":" + username + ":" + lockType;
    }

    /**
     * 校验账号密码，并检查最大错误次数
     *
     * @param user
     */
    public void validate(SysUser user) {
        validate(user.getPassword(), maxRetryCount, lockTime, TimeUnit.MINUTES, DEFAULT_LOCK_TYPE);
    }

    public void validate(String encodedPassword) {
        validate(encodedPassword, maxRetryCount, lockTime, TimeUnit.MINUTES, DEFAULT_LOCK_TYPE);
    }

    public void validate(String encodedPassword,
                         Integer maxRetryCount,
                         Integer lockTime,
                         TimeUnit timeUnit,
                         String lockType) {
        String username = getLoginUserName();
        String password = getLoginPassword();
        //获取到当前用户错误次数，没有就为0
        UserPasswordRetryLimitInfo userPasswordRetryLimitInfo = null;
        userPasswordRetryLimitInfo = redisCache.getCacheObject(getCacheKey(username, lockType));
        if (ObjectUtil.isEmpty(userPasswordRetryLimitInfo)) {
            userPasswordRetryLimitInfo = UserPasswordRetryLimitInfo.builder()
                    .maxRetryCount(maxRetryCount)
                    .lockTime(lockTime)
                    .timeUnit(timeStringMap.get(timeUnit))
                    .count(0)
                    .lockType(lockType)
                    .createTime(new Date())
                    .build();
        }
        Integer retryCount = userPasswordRetryLimitInfo.getCount();
        lockTime = userPasswordRetryLimitInfo.getLockTime();
        maxRetryCount = userPasswordRetryLimitInfo.getMaxRetryCount();
        String timeUnitType = userPasswordRetryLimitInfo.getTimeUnit();
        //判断当前如果已经大于等于最大错误次数，即抛出锁定异常提示
        if (retryCount >= Integer.valueOf(maxRetryCount).intValue()) {
            recordPasswordRetryLimit(username);
            throw new UserPasswordRetryLimitExceedException(maxRetryCount, lockTime, timeUnitType);
        }
        //如果本次密码错误，错误次数加一，更新缓存
        if (!matches(encodedPassword, password)) {
            retryCount = retryCount + 1;
            userPasswordRetryLimitInfo.setCount(retryCount);
            recordPasswordNotMatch(username, retryCount);
            redisCache.setCacheObject(getCacheKey(username, lockType), userPasswordRetryLimitInfo, lockTime, timeUnit);
            throw new UserPasswordNotMatchException();
        } else {
            //密码正确就清空错误计数
            clearLoginRecordCache(username, lockType);
        }
    }


    protected String getLoginUserName() {
        Authentication usernamePasswordAuthenticationToken = AuthenticationContextHolder.getContext();
        //获取账号
        return usernamePasswordAuthenticationToken.getName();
    }

    protected String getLoginPassword() {
        Authentication usernamePasswordAuthenticationToken = AuthenticationContextHolder.getContext();
        //获取密码
        return usernamePasswordAuthenticationToken.getCredentials().toString();
    }

    protected void recordPasswordRetryLimit(String username) {
        AsyncManager.me().execute(AsyncFactory.recordLogininfor(username, Constants.LOGIN_FAIL,
                MessageUtils.message("user.password.retry.limit.exceed", maxRetryCount, lockTime)));
    }

    protected void recordPasswordNotMatch(String username, Integer retryCount) {
        AsyncManager.me().execute(AsyncFactory.recordLogininfor(username, Constants.LOGIN_FAIL,
                MessageUtils.message("user.password.retry.limit.count", retryCount)));
    }

    protected boolean matches(String encodedPassword, String rawPassword)
    {
        return SecurityUtils.matchesPassword(rawPassword, encodedPassword);
    }

    public void clearLoginRecordCache(String loginName, String lockType)
    {
        if (redisCache.hasKey(getCacheKey(loginName, lockType)))
        {
            redisCache.deleteObject(getCacheKey(loginName, lockType));
        }
    }

    public void clearLoginRecordCache(String loginName)
    {
        clearLoginRecordCache(loginName, DEFAULT_LOCK_TYPE);
    }
}

@Data
@AllArgsConstructor
@NotNull
@Builder
class UserPasswordRetryLimitInfo implements Serializable {

    static final long serialVersionUID = 46145432L;

    /** 第一次错误的时间 **/
    @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
    private Date createTime;

    /** 锁定类型 **/
    private String lockType;

    /** 时间单位 **/
    private String timeUnit;

    /** 最大尝试次数 **/
    private Integer maxRetryCount;

    /** 锁定时间 **/
    private Integer lockTime;

    /** 失败次数 **/
    private Integer count;
}
